package de.hmm.hwip.connection.output;

/**
 *  Outgoing Connection Thread
 * 	establishes connection to network devices
 * 	for each connection there is an own thread 
 * 
 * @author johannes mendel
 * @version 31.10.2012
 *
 */
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.util.Date;

import javax.crypto.spec.SecretKeySpec;

import org.json.JSONException;
import org.json.JSONObject;
import org.osgi.framework.BundleContext;
import org.osgi.util.tracker.ServiceTracker;

import android.content.Context;
import android.content.SharedPreferences;
import android.util.Base64;
import android.util.Log;
import de.hmm.hwip.security.api.ISecurityClient;

public class OutConThread implements Runnable {

	/** Connection socket of receiver**/
	private Socket connection;
	
	/** data to send **/
	private JSONObject data;
	
	/** identifier if datas will be encrypted **/
	private boolean securityFlag;
	
	/** class identifier for debug **/
	private final String TAG = "OutConThread (connect. service)";
	
	/** addresses if a message will be send to multiple receivers **/
	private String[] targets = null;
	
	/** address of receiver **/
	private String target = null;
	
	/** OSGi Bundlecontext **/
	private BundleContext context;
	
	/** reconnection counter **/
	private int reconCounter = 0;

	/** android service Context **/
	private Context androidContext;
	
	/** service for security client site */
	private ISecurityClient serviceClientSecurity;
	
	/** Client Security service tracker */
	private ServiceTracker secuServiceTracker;
	
	/**
	 * Constructor for reused connections
	 *  - security need in some cases
	 * @param targets - String array with target ip addresses
	 * @param data - Jsonobject with data to send
	 * @param securityFlag - identifier if data will be encrypted
	 */
	public OutConThread(Socket connection, JSONObject data, boolean securityFlag, BundleContext context){
		this.connection = connection;
		this.data = data;
		this.securityFlag = securityFlag;
		this.context = context;
	}
	
	/**
	 * Constructor for burst messages
	 *  - no security need
	 * @param targets - String array with target ip addresses
	 * @param data - Jsonobject with data to send
	 */
	public OutConThread(String[] targets, JSONObject data, BundleContext context){
		this.targets = targets;
		this.data = data;
		this.context = context;
	}
	
	/**
	 * Constructor to search for gateway 
	 * @param data - Jsonobject with data to send
	 * @param context - OSgi bundle context
	 */
	public OutConThread(JSONObject data, BundleContext context, String target){
		this.target = target;
		this.context = context;
		this.data = data;
	}
	
	/**
	 * Constructor to send datas between specified spot and gateway
	 *  - security needed because of sensitive data
	 * @param target - target spot or gateway
	 * @param data - Jsonobject with data to send
	 * @param securityFlag - true if security is need
	 */
	public OutConThread(String target, JSONObject data, Context androidContext, boolean securityFlag, BundleContext context){
		this.target = target;
		this.data = data;
		this.securityFlag = securityFlag;
		this.androidContext = androidContext;
		this.context = context;
	}
	
	@Override
	public void run() {
		Log.i(TAG, "Thread started");
		
		secuServiceTracker = new ServiceTracker(this.context, ISecurityClient.class.getName(), null);
		secuServiceTracker.open();
		
		while(true){
			serviceClientSecurity = (ISecurityClient) secuServiceTracker.getService();
			if(serviceClientSecurity != null){
				break;
			}else{
				try {
					wait(2000);
				} catch (InterruptedException e) {
					Log.e(TAG, "wait for Client Security Service exception: " + e.getMessage());
				}
			}
		}
		
		if(connection == null){
			if(data.has("command") && data.has("value") && data.has("param")){
				try {
					if(data.get("command").equals("existspot"))
						searchGateway(data.getString("value"));
				} catch (JSONException e) {
					Log.e(TAG, "Json Exception on get('command')" + e.getMessage());
				}
			}else if(targets != null){
				sendBurstMessage();
			}else if(target != null){
				sendDataMessage();
			}
		}else if(connection != null && data != null){
			answerDevice(refreshConnection());
		}
	}
	
	/** Method to search the Gateway **/
	public void searchGateway(String ipAdress){
		Log.i(TAG,"search Gateway from: " + ipAdress);
		//connection socket
		Socket socket = new Socket();
		//Outputstream
		PrintWriter bos = null;
		//identifier if socket is connected
		boolean isConnected = false;
		//Ip area where spot searches for Gateway
		String ipArea = null;
		//Ip terminal
		int terminal = 200;
		
		//extracts sub Ip area
		try{
			String[] adressField = new String[4];
			adressField = (ipAdress.split("\\."));
			ipArea = adressField[0] + "." + adressField[1] + "." + adressField[2] + ".";
		}catch(IndexOutOfBoundsException e){
			Log.e(TAG, "Ip-transformation failed: " + e.getMessage());
		}

		//searching for Gateways Ipadress
		SocketAddress sockaddr;
		InetAddress addr;
		while(!isConnected){
			try {	  
				Log.i(TAG, "Connect to adress: " +ipArea+ terminal);
				addr = InetAddress.getByName((ipArea + terminal));
				if(target != null && terminal == 0){
					addr = InetAddress.getByName(target);
					target = null;
					terminal = -1;
				}
				sockaddr = new InetSocketAddress(addr, 7600);
				socket.connect(sockaddr, 20000); //5 second connection timeout
				if(socket.isConnected()){
					isConnected = true;
					break;
				}
			}catch (SocketTimeoutException e){
				Log.w(TAG, "Address does not match: " + ipArea + terminal);
				//isConnected = false;
			}catch (IOException e) {
				Log.e(TAG, "Socket Error: " + ipArea + terminal + ", " + e.getMessage());
				socket = new Socket();
				//isConnected = false;
			}
			terminal++;
			if(terminal >= 255)
				break;
		}
		
		//send command to Gateway
		try{
			if (socket.isConnected()) { 
				bos = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true);
				// Outputstring
				String out = data.toString();
				// answer on stream
				bos.println(out);
				// EOF signal 
				bos.println("#.#");
				bos.close();
				//close connection
				try {
					Log.i(TAG,"close socket");
					socket.close();
				} catch (IOException e) {
					Log.e(TAG, "Socket close exception: " + e.getMessage());
				}
			}
		}catch (IOException e) {
			Log.e(TAG, "Output Stream Error: " + e.getMessage());
		}finally{
				//close outstream
			if(bos != null)
				bos.close();
				//close connection
				try {
					socket.close();
				} catch (IOException e) {
					Log.e(TAG, "Socket close exception: " + e.getMessage());
				}
		}

	}
	
	/** Method to send datas to other devices **/
	private void answerDevice(Socket socket){			
		// stream for answer
		PrintWriter bos = null;
		
		if (socket.isConnected()) { 
			Log.i(TAG, "send to: " + socket.getInetAddress().toString() + ":" + socket.getPort() + " -> " + data);
			try
			{
				// generate the answer stream
				bos = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true);
				
				// we have make a encryption of the answer
				String out = "";
				
				//crypt if securityFlag is true
		    	if(securityFlag)
		    	{
		    		byte[] aesKey;
					try {
						Log.i(TAG, "secure connection!");
						aesKey = this.handshake();
						SecretKeySpec skeySpec = new SecretKeySpec(aesKey, "AES");
						serviceClientSecurity.setAESKey(skeySpec);
						out = serviceClientSecurity.encryptStringAES(data.toString());
					} catch (Exception e) {
						Log.e(TAG, "handshake exception: " + e.getLocalizedMessage());
					}
		    	}
		    	else
		    	{
		    		out = data.toString();
		    	}
				
				// answer on stream
				bos.println(out);
				// EOF signal 
		    	bos.println("#.#");
		    	
			}catch (IOException e) {
				Log.e(TAG, "Output Stream Error: " + e.getMessage());
			}finally{
				try {
					//close outstream
					bos.close();
					//close connection
					socket.close();
					Log.i(TAG, "Socket closed");
				} catch (IOException e) {
					Log.e(TAG, "Close Connection Error: " + e.getMessage());
				}
			}
		}
	}
	
	/** send datas to multiple receivers **/
	private void sendBurstMessage(){
		//receiver
		//connection socket
		Socket socket = new Socket();
		SocketAddress sockaddr;
		
		for(String ipAdress : targets){
			if(ipAdress != null){
				try {	  
					sockaddr = new InetSocketAddress(ipAdress, 7601);
					socket.connect(sockaddr, 10000); //5 second connection timeout
					answerDevice(socket);
				}catch (SocketTimeoutException e){
					Log.w(TAG, "Adress does not answer: " + ipAdress);
				}catch (IOException e) {
					Log.e(TAG, "Socket Error: " + e.getMessage());
				}
				socket = new Socket();
			}
		}
	}
	
	/** method to send Data on spot-port **/
	private void sendDataMessage(){
		//receiver
		//connection socket
		Socket socket = new Socket();
		SocketAddress sockaddr;
	
		try {	  
			sockaddr = new InetSocketAddress(target, 7601);
			socket.connect(sockaddr, 10000); //10 second connection timeout
			answerDevice(socket);
		}catch (SocketTimeoutException e){
			Log.w(TAG, "Adress does not answer: " + target);
		}catch (IOException e) {
			Log.e(TAG, "Socket Error: " + e.getMessage());
		}
	}

	/** method to refresh closed connections **/
	private Socket refreshConnection(){
		Log.i(TAG, "Try to refresh connection with: " + target);
		Socket socket = new Socket();
		try {
			InetAddress addr = connection.getInetAddress(); //InetAddress.getByName(target);
			SocketAddress sockaddr = new InetSocketAddress(addr, 7601);
			socket.connect(sockaddr, 10000);
		} catch (IOException e) {
			reconCounter++;
			Log.w(TAG, "Connection Failed: " + e.getMessage());
			if(reconCounter <= 10){
				try {
					socket.close();
				} catch (IOException e1) {
					Log.w(TAG, "doesnt work");
				}
				refreshConnection();
			}
		}
		return socket;
	}
	
	/**
	 * Method to check if the aes key is valid (and not older then 24h) 
	 * 
	 * if not a new key will be requested from gateway
	 * 
	 * @return A valid AES-Key
	 */
	@SuppressWarnings("deprecation")
	private byte[] handshake() throws Exception
	{
		// getting the preferences
		SharedPreferences preferences = androidContext.getSharedPreferences("security", Context.MODE_PRIVATE);
		String key = preferences.getString("AES_KEY", null);
		long keyDate = preferences.getLong("AES_KEY_DATE", Long.MAX_VALUE);
		Date date = new Date(keyDate);
		Date today = new Date();
		boolean equal = false;

		// compare dates using day accuracy
		if(date.getDay() == today.getDay() && date.getMonth() == today.getMonth() && date.getYear() == today.getYear())
			equal = true;
		
		// Case 1: First handshake (key and date == null)
		// Case 2: Key is there but old.
		if(key == null || keyDate == Long.MAX_VALUE || !equal){
			JSONObject tempdata = new JSONObject();
			tempdata.put("temp", data);
			getValidKey();
			synchronized (this) {
				this.wait(7000);
			}
			
			securityFlag = true;
			data = tempdata.getJSONObject("temp");
			
			handshake();
		}

		// Case 3: key is valid (yey)
		return Base64.decode(key, Base64.DEFAULT);
	}
	
	/** Requests the aes key from gateway */
	private void getValidKey() throws Exception
	{
		data = new JSONObject();
		// assembling the message
		data.put("service", "hwip.processing.Spot");
		data.put("command", "getaeskey");
		securityFlag = false;
		
		sendDataMessage();
	}
}
